#!/usr/bin/env python3

from __future__ import print_function
import sys
import os
import xml.etree.ElementTree as ET
import getopt

script_dir = os.path.dirname(os.path.realpath(__file__))
gltree = ET.parse(os.path.join(script_dir, '../../../thirdparty/opengl-registry/xml/gl.xml'))
egltree = ET.parse(os.path.join(script_dir, '../../../thirdparty/egl-registry/api/egl.xml'))
glroot = gltree.getroot()
eglroot = egltree.getroot()
glcommands = {}
eglcommands = {}

# Parser for Khronos XML
def khronos_xml_parser(root, commands):
    for command in root.find('commands'):
        d = {}
        proto = command.find('proto')
        d['return_type_str'] = ''.join([text for text in proto.itertext()][:-1]).strip()
        d['function_name'] = proto.find('name').text
        d['name_list'] = list()
        for param in command.findall('param/name'):
            d['name_list'].append(param.text)
        p = [{"strlist": [text.strip() for text in param.itertext() if text.strip()],
              "length": param.attrib.get('len')}
            for param in command.findall('param')]

        for param in p:
            splitted = []
            for string in param['strlist']:
                for word in string.split():
                    splitted.append(word)
            param['strlist'] = splitted

        d['parameters'] = p

        commands[d['function_name']] = d

# Parse commands from XML and store them in globals
khronos_xml_parser(glroot, glcommands)
khronos_xml_parser(eglroot, eglcommands)

############################################################################
# generateSourceFile
def generateSourceFile(protos, folder, fname, pythonCmd, manual_imp_funcs, includes=[]):
    dirname = os.path.join(os.path.dirname(os.path.realpath(__file__)), folder)
    filename = os.path.join(dirname, fname)

    keys = list(protos.keys())
    keys.sort()

    if not os.path.exists(dirname):
        os.makedirs(dirname)

    with open(filename, "w") as f:
        f.write('// This code is auto-generated by: \n')
        f.write('// '+pythonCmd+'\n')
        f.write('#include "../common.h"\n\n')

        for include in includes:
            f.write('#include "' + include + '"\n')

        f.write('#include <unistd.h>\n')
        f.write('#include "dispatch/gleslayer_helper.h"\n')

        f.write("\nextern \"C\" {\n\n")

        if folder == 'gles1':
            f.write("typedef struct __GLsync *GLsync;\n")
            f.write("typedef uint64_t GLuint64;\n")
            f.write("typedef int64_t GLint64;\n")
        elif folder != 'egl':
            f.write("typedef GLDEBUGPROCKHR GLDEBUGPROC;\n")
            f.write("typedef void (*GLVULKANPROCNV)(void);\n")
        f.write('\n')

        # Output the function bodies of the intercept later
        # Each function must call the plugin version of the function
        for i in keys:
            if i in manual_imp_funcs:
                continue
            command = protos[i]
            param = [' '.join(p['strlist']) for p in command['parameters']]
            command['param_string'] = ', '.join(param)
            f.write('typedef {return_type_str} (*FUNCPTR_{function_name})({param_string});\n'.format(**command))
        f.write("\n")

        for i in keys:
            if i in manual_imp_funcs:
                continue
            command = protos[i]
            f.write('static FUNCPTR_{function_name} sp_{function_name} = 0;\n'.format(**command))
        f.write("\n")

        for i in keys:
            if i in manual_imp_funcs:
                continue
            command = protos[i]
            f.write('static bool warned_{function_name} = false;\n'.format(**command))
        f.write("\n")

        f.write("/// force new function lookups\n")
        f.write("__attribute__ ((unused)) static void fakedriverReset()\n{\n")
        for i in keys:
            if i in manual_imp_funcs:
                continue
            command = protos[i]
            f.write('    sp_{function_name} = 0;\n'.format(**command));
            f.write('    warned_{function_name} = false;\n'.format(**command));
        f.write("}\n\n")

        for i in keys:
            if i in manual_imp_funcs:
                continue
            command = protos[i]
            param = [' '.join(p['strlist']) for p in command['parameters']]
            command['param_string'] = ', '.join(param)
            f.write('PUBLIC {return_type_str} {function_name}({param_string});\n'.format(**command))
        f.write("\n")

        for i in keys:
            if i in manual_imp_funcs:
                continue
            command = protos[i]
            param = [' '.join(p['strlist']) for p in command['parameters']]
            command['param_string'] = ', '.join(param)
            command['call_list'] = ', '.join(command['name_list'])

            real_call = 'tmp({call_list})'.format(**command)
            returnValue = '{return_type_str}'.format(**command)
            hasReturnValue = returnValue.lower() != "void" or not returnValue

            f.write('#ifdef GLESLAYER\n')
            f.write('EGLAPI {return_type_str} EGLAPIENTRY glesLayer_{function_name}({param_string})\n'.format(**command))
            f.write('#else\n')
            f.write('{return_type_str} {function_name}({param_string})\n'.format(**command))
            f.write('#endif\n')
            f.write('{\n')
            if i == 'eglInitialize':
                f.write("    fakedriverReset(); // forcibly break any OS caching of these functions\n")
            # need temporary for thread-safety in case of reentrancy or dispatch table is cleared
            f.write('    FUNCPTR_{function_name} tmp = sp_{function_name};\n'.format(**command))
            f.write('    if (tmp != 0)\n')
            f.write('    {\n')
            if i == 'eglSwapBuffers':
                f.write('        if (wrapper::CWrapper::sShowFPS)\n')
                f.write('        {\n')
                f.write('            gFpsLog.SwapBufferHappens();\n')
                f.write('        }\n')
            f.write('        return ' + real_call + ';\n')
            f.write('    }\n')
            f.write('    tmp = (FUNCPTR_{function_name}) wrapper::CWrapper::GetProcAddress("{function_name}");\n'.format(**command))
            f.write('    if (tmp == 0)\n')
            f.write('    {\n')
            if i.startswith('eglSwapBuffersWithDamage'):
                # On the Nexus 6P with Android N, the Adreno drivers does not support eglSwapBuffersWithDamageKHR;
                # eglGetProcAddress(..) will return NULL and the symbol is not exported by the lib.
                # However, the fakdriver *does* export this symbol, and it appears Android N will try to call this
                # function despite first trying eglGetProcAddress(...) which properly returns NULL.
                # The result is that no swapping occurs, and the Android UI isn't updated on the phone.
                # As a workaround, we here try calling 'normal' eglSwapBuffers() if WithDamage* is NULL but
                # is still called.
                f.write('        if (!warned_{function_name}) DBG_LOG("Warning: Fakedriver/Gleslayer failed to get function pointer for {function_name}. eglSwapBuffers() will be called instead.\\n");\n'.format(**command))
                f.write('        // Try calling normal eglSwapBuffers(). See autogencode.py for details why.\n')
                f.write('        eglSwapBuffers(dpy, surface);\n')
            else:
                f.write('        if (!warned_{function_name}) DBG_LOG("Warning: Fakedriver/Gleslayer failed to get function pointer for {function_name}\\n");\n'.format(**command))
            f.write('        warned_{function_name} = true;\n'.format(**command))
            if (hasReturnValue):
                f.write('        return 0;\n')
            else:
                f.write('        return;\n')
            f.write('    }\n')
            f.write('    sp_{function_name} = tmp;\n'.format(**command))
            f.write('    return ' + real_call + ';\n')
            f.write("}\n\n")
        f.write("\n")

        f.write('#ifdef GLESLAYER\n')
        f.write('LAYER_DATA gPatraceLayer;\n')
        f.write('\n')

        f.write('EGLAPI EGLFuncPointer EGLAPIENTRY glesLayer_eglGetProcAddress(const char *procName)\n')
        f.write('{\n')
        f.write('    EGLFuncPointer func = reinterpret_cast<EGLFuncPointer>(dispatch_intercept_func(PATRACE_LAYER_NAME, "eglGetProcAddress"));\n')
        f.write('    //DBG_LOG("glesLayer_eglGetProcAddress(%s): wrapper intercept (eglGetProcAddress) %p\\n", procName, func);\n')
        f.write('    if (func == NULL)    return NULL;\n')
        f.write('    typedef EGLFuncPointer (* FUNCPTR_eglGetProcAddress)(const char *);\n')
        f.write('    FUNCPTR_eglGetProcAddress gpa = reinterpret_cast<FUNCPTR_eglGetProcAddress>(func);\n')
        f.write('    return gpa(procName);\n')
        f.write('}\n\n')

        f.write('static void glesLayer_InitializeLayer(void * layer_id,\n')
        f.write('       PFNEGLGETNEXTLAYERPROCADDRESSPROC get_next_layer_proc_address) \n')
        f.write('{\n')
        f.write('    DBG_LOG("glesLayer_InitializeLayer called with layer_id %p get_next_layer_proc_address %p, pid(%d).\\n", layer_id, get_next_layer_proc_address, getpid());\n\n')
        f.write('    gLayerCollector[std::string(PATRACE_LAYER_NAME)] = &gPatraceLayer;\n\n')
        f.write('    layer_init_next_proc_address(PATRACE_LAYER_NAME, layer_id, get_next_layer_proc_address);\n\n')
        f.write('    layer_init_intercept_map();\n')
        f.write('}\n\n')

        f.write('static EGLFuncPointer glesLayer_patrace_eglGPA(const char* funcName) \n')
        f.write('{\n')
        f.write('\n#define GETPROCADDR(func) if (!strcmp(funcName, #func)) { \\\n')
        f.write('return (EGLFuncPointer) glesLayer_##func; \\\n')
        f.write('}\n\n')
        for i in keys:
            if i in ['eglStreamConsumerGLTextureExternalAttribsNV']:
                continue
            command = protos[i]
            f.write('    GETPROCADDR({function_name});\n'.format(**command))
        f.write('    return NULL;\n')
        f.write('}\n\n')

        f.write('static EGLFuncPointer glesLayer_GetLayerProcAddress(const char *funcName, EGLFuncPointer next)\n')
        f.write('{\n')
        f.write('\n')
        f.write('    //here return the gles layer entrypoint glesLayer_glxxx() as the new hook into EGL Loader\n')
        f.write('    EGLFuncPointer entry = glesLayer_patrace_eglGPA(funcName);\n')
        f.write('\n')
        f.write('    if (entry != NULL)\n')
        f.write('    {\n')
        f.write('        return entry;\n')
        f.write('    }\n\n')
        f.write('    // If gles Layer does not hook the function, just return original func pointer.\n')
        f.write('    DBG_LOG("GLES Layer does not hook the proc %s, just return original next func pointer %p.\\n", funcName, next);\n')
        f.write('    return next;\n')
        f.write('}\n\n')

        f.write('//\n')

        f.write('__attribute((visibility("default"))) void AndroidGLESLayer_Initialize(void* layer_id,\n')
        f.write('           PFNEGLGETNEXTLAYERPROCADDRESSPROC get_next_layer_proc_address) {\n')
        f.write('    return (void)glesLayer_InitializeLayer(layer_id, get_next_layer_proc_address);\n')
        f.write('}\n')

        f.write('__attribute((visibility("default"))) void* AndroidGLESLayer_GetProcAddress(\n')
        f.write('           const char *funcName, EGLFuncPointer next) {\n')
        f.write('    return (void*)glesLayer_GetLayerProcAddress(funcName, next);\n')
        f.write('}\n')

        f.write('#endif // GLESLAYER\n')

        f.write("} // end of extern C\n")

        f.write('\n')

    print('Generated %s/%s' % (folder, fname))

    return 0

def generateHelperFile(protos, folder, fname, pythonCmd, manual_imp_funcs, includes=[]):
    dirname = os.path.join(os.path.dirname(os.path.realpath(__file__)), folder)
    filename = os.path.join(dirname, fname)

    keys = list(protos.keys())
    keys.sort()

    with open(filename, "w") as f:
        f.write('// This code is auto-generated by: \n')
        f.write('// '+pythonCmd+'\n')

        f.write('#include "os_string.hpp"\n')
        f.write('#include "gleslayer_helper.h"\n')

        f.write('std::unordered_map<std::string, LAYER_DATA *> gLayerCollector;\n')
        f.write('\n')
        f.write("\nextern \"C\" {\n\n")

        f.write('void* dispatch_intercept_func(const char *layerName, const char* procName)\n')
        f.write('{\n')
        f.write('    LAYER_DATA *layer = gLayerCollector[std::string(layerName)];\n')
        f.write('    void *tmp = NULL;\n\n')
        f.write('    if (layer->interceptFuncMap.find(procName) != layer->interceptFuncMap.end())\n')
        f.write('    {\n')
        f.write('        tmp = reinterpret_cast<void *>(layer->interceptFuncMap[std::string(procName)]);\n')
        f.write('    }\n\n')
        f.write('    if (!tmp)\n')
        f.write('    {\n')
        f.write('        DBG_LOG("dispatch_intercept_func(%s): Not support to trace proc %s, and calling from next layer.\\n", procName, procName);\n')
        f.write('        tmp = reinterpret_cast<void *>(layer->nextLayerFuncMap[std::string(procName)]);\n')
        f.write('    }\n\n')
        f.write('    return tmp;\n')
        f.write('}\n\n')

        f.write('void layer_init_next_proc_address(const char *layerName, void *layer_id,\n')
        f.write('                          PFNEGLGETNEXTLAYERPROCADDRESSPROC get_next_layer_proc_address)\n')
        f.write('{\n')
        f.write('    LAYER_DATA *layer = (gLayerCollector[std::string(layerName)]);\n')
        for i in keys:
            command = protos[i]
            f.write('    layer->nextLayerFuncMap[std::string("{function_name}")] = reinterpret_cast<EGLFuncPointer>(get_next_layer_proc_address(layer_id, "{function_name}"));\n'.format(**command))
        f.write('    DBG_LOG("layer_init_netx_proc_address called with layer(%s) layer_id %p get_next_layer_proc_address %p, pid(%d)\\n", layerName, layer_id, get_next_layer_proc_address, getpid());\n')
        f.write('}\n\n')

        f.write('} //extern C\n')
    return 0

def GenerateWrapper(headerDir, pythonCmd, manual_imp_funcs):
    all_commands = {}
    all_includes = list()

    # EGL
    sum_commands = {}
    includes = ['EGL/egl.h', 'EGL/eglext.h', 'fps_log.hpp']
    generateSourceFile(eglcommands, 'egl', 'auto.cpp', pythonCmd, manual_imp_funcs, includes)
    all_commands.update(eglcommands)
    all_includes.extend(includes)

    # GLES2+
    # We need to include GLES2+ before GLES1 because glext.h contains a subset of KHR_debug
    # but with identical include guards...
    sum_commands = {}
    includes = ['GLES2/gl2.h', 'GLES2/gl2ext.h', 'GLES3/gl3.h', 'GLES3/gl31.h', 'GLES3/gl32.h']
    for version in ['GL_ES_VERSION_2_0', 'GL_ES_VERSION_3_0', 'GL_ES_VERSION_3_1', 'GL_ES_VERSION_3_2']:
        for command in glroot.findall("feature[@name='{v}']/require/command".format(v=version)):
            command_name = command.get('name')
            sum_commands[command_name] = glcommands[command_name]
    for ext in glroot.findall("extensions/extension"):
        if 'gles2' in ext.get('supported') or 'gles3' in ext.get('supported'):
            for require in ext.findall("require"):
                api = require.get('api', None)
                if api != None and api != 'gles2' and api != 'gles3':
                    continue
                for command in require.findall("command"):
                    command_name = command.get('name')
                    sum_commands[command_name] = glcommands[command_name]
    generateSourceFile(sum_commands, 'gles2', 'auto.cpp', pythonCmd, manual_imp_funcs, includes)
    all_commands.update(sum_commands)
    all_includes.extend(includes)

    # GLES1
    sum_commands = {}
    includes = ['GLES/gl.h', 'GLES/glext.h']
    for version in ['GL_VERSION_ES_CM_1_0']:
        for command in glroot.findall("feature[@name='{v}']/require/command".format(v=version)):
            command_name = command.get('name')
            sum_commands[command_name] = glcommands[command_name]
    for ext in glroot.findall("extensions/extension"):
        if 'gles1' in ext.get('supported'):
            for require in ext.findall("require"):
                api = require.get('api', None)
                if api != None and api != 'gles1':
                    continue
                for command in require.findall("command"):
                    command_name = command.get('name')
                    sum_commands[command_name] = glcommands[command_name]
        elif not ext.get('supported'):
            print('No supported attribute for extension %s' % ext.get('name'))
    generateSourceFile(sum_commands, 'gles1', 'auto.cpp', pythonCmd, manual_imp_funcs, includes)
    all_commands.update(sum_commands)
    all_includes.extend(includes)

    # AFRC
    afrc_commands = {}
    d={}
    d['return_type_str'] = 'void'
    d['function_name'] = 'glTexStorageAttribs2DARM'
    d['name_list'] = list()
    for name in ['target', 'levels', 'internalformat', 'width', 'height', 'attrib_list']:
        d['name_list'].append(name)
    p = [{"strlist":['GLenum target'], "length":4}, {"strlist":['GLsizei levels'], "length":4}, {"strlist":['GLenum internalformat'], "length":4},
         {"strlist":['GLsizei width'], "length":4}, {"strlist":['GLsizei height'], "length":4}, {"strlist":['const int *attrib_list'], "length":4}
        ]
    d['parameters'] = p
    afrc_commands['glTexStorageAttribs2DARM'] = d

    d={}
    d['return_type_str'] = 'void'
    d['function_name'] = 'glTexStorageAttribs3DARM'
    d['name_list'] = list()
    for name in ['target', 'levels', 'internalformat', 'width', 'height', 'depth', 'attrib_list']:
        d['name_list'].append(name)
    p = [{"strlist":['GLenum target'], "length":4}, {"strlist":['GLsizei levels'], "length":4}, {"strlist":['GLenum internalformat'], "length":4},
         {"strlist":['GLsizei width'], "length":4}, {"strlist":['GLsizei height'], "length":4}, {"strlist":['GLsizei depth'], "length":4},
         {"strlist":['const int *attrib_list'], "length":4}
        ]
    d['parameters'] = p
    afrc_commands['glTexStorageAttribs3DARM'] = d

    all_commands.update(afrc_commands)

    # single .so file
    generateSourceFile(all_commands, 'single', 'auto.cpp', pythonCmd, manual_imp_funcs, all_includes)

    # generate gleslayer_helper.cpp
    generateHelperFile(all_commands, '../dispatch', 'gleslayer_helper.cpp', pythonCmd, manual_imp_funcs, includes)

    return 0

def usage():
    print("Generate wrapper libraries based on khronos XML")
    print("Options:")
    print("-h Path to the khronos headers (Default = '../../../thirdparty/opengl-registry/api/'")

if __name__ == '__main__':
    opts = None
    args = None
    script_dir = os.path.dirname(os.path.realpath(__file__))
    headerDir   = os.path.join(script_dir, "../../../thirdparty/opengl-registry/api")

    try:
        opts,args = getopt.getopt(sys.argv[1:], "h:", ["help"])
    except getopt.GetoptError as err:
        print(str(err))
        usage()
        sys.exit(0)

    # Parse options:
    for o, a in opts:
        print(o, a)
        if o == '-h':
            headerDir = a
        elif o == "--help":
            usage()
            sys.exit(0)

    pythonCmd = 'python autogencode.py'
    manual_imp_funcs = [ 'eglGetProcAddress', 'eglStreamConsumerGLTextureExternalAttribsNV' ]
    GenerateWrapper(headerDir, pythonCmd, manual_imp_funcs)
